/*
 * $Id: playlist.c 1111 2009-08-30 06:53:01Z jasta00 $
 *
 * Copyright (C) 2008 Josh Guilfoyle <jasta@devtcg.org>
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2, or (at your option) any
 * later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 */

#include "main.h"

#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

#include <sqlite3.h>

#include "db.h"
#include "playlist.h"

/*****************************************************************************/

static void mark_pending_delete(sqlite3 *dbh, sqlite_int64 _id,
  gboolean value)
{
	sqlite3_stmt *stmt;

	int rc = sqlite3_prepare(dbh, "UPDATE playlists SET pending_delete = ? WHERE _id = ?", -1, &stmt, NULL);
	assert(rc == SQLITE_OK);

	sqlite3_bind_int64(stmt, 1, (value == TRUE) ? 1 : 0);
	sqlite3_bind_int64(stmt, 2, _id);

	rc = sqlite3_step(stmt);
	assert(rc == SQLITE_DONE);

	sqlite3_finalize(stmt);
}

static void tick(sqlite3 *dbh, sqlite_int64 _id)
{
	sqlite3_stmt *stmt;

	int rc = sqlite3_prepare(dbh, "UPDATE playlists SET tick = 1 WHERE _id = ?", -1, &stmt, NULL);
	assert(rc == SQLITE_OK);

	sqlite3_bind_int64(stmt, 1, _id);

	rc = sqlite3_step(stmt);
	assert(rc == SQLITE_DONE);

	sqlite3_finalize(stmt);
}

static char *derive_playlist_name(const char *path)
{
	char *name;
	char *ext;

	if ((name = g_path_get_basename(path)) == NULL)
		return NULL;

	if ((ext = strrchr(name, '.')) != NULL)
		ext[0] = '\0';

	return name;
}

static sqlite_int64 find_playlist(sqlite3 *dbh, const char *path)
{
	sqlite_int64 _id = -1;
	sqlite3_stmt *stmt;
	int rc;

	rc = sqlite3_prepare(dbh, "SELECT _id, pending_delete FROM playlists WHERE filename = ?", -1, &stmt, NULL);
	assert(rc == SQLITE_OK);

	sqlite3_bind_text(stmt, 1, path, -1, NULL);

	if ((rc = sqlite3_step(stmt)) == SQLITE_ROW)
	{
		gboolean pending;

		_id = sqlite3_column_int64(stmt, 0);
		assert(_id >= 0);

		pending = (sqlite3_column_int(stmt, 1) == 0) ? FALSE : TRUE;

		if (pending == TRUE)
			mark_pending_delete(dbh, _id, FALSE);
	}

	sqlite3_finalize(stmt);

	return _id;
}

static sqlite_int64 insert_playlist(sqlite3 *dbh, const char *path)
{
	sqlite3_stmt *stmt;
	int rc;

	char *name = derive_playlist_name(path);

	rc = sqlite3_prepare(dbh,
"INSERT INTO playlists "
	"(name, filename, created_date, tick) "
"VALUES"
	"(?, ?, ?, ?)",
	-1, &stmt, NULL);
	assert(rc == SQLITE_OK);

	sqlite3_bind_text(stmt, 1, name, -1, g_free);
	sqlite3_bind_text(stmt, 2, path, -1, SQLITE_STATIC);
	sqlite3_bind_int64(stmt, 3, (sqlite_int64)time(NULL));
	sqlite3_bind_int(stmt, 4, 1);

	if ((rc = sqlite3_step(stmt)) != SQLITE_DONE)
	{
		g_warning("Couldn't insert playlist '%s': %s\n",
		  path, sqlite3_errmsg(dbh));
		sqlite3_finalize(stmt);

		return -1;
	}

	sqlite3_finalize(stmt);
	return sqlite3_last_insert_rowid(dbh);
}

static sqlite_int64 insert_or_tick_playlist(sqlite3 *dbh, sqlite_int64 _id,
  const char *path)
{
	if (_id > 0)
		tick(dbh, _id);
	else
	{
		if ((_id = insert_playlist(dbh, path)) == -1)
		{
			g_warning("Failed to insert playlist for '%s'", path);
			return -1;
		}
	}

	return _id;
}

static gboolean has_playlist_song_at_pos(sqlite3 *dbh, sqlite_int64 _id,
  sqlite_int64 song_id, int pos)
{
	sqlite3_stmt *stmt;
	int rc;
	gboolean ret;

	rc = sqlite3_prepare(dbh, "SELECT _id FROM playlist_songs WHERE playlist_id = ? AND song_id = ? AND position = ?", -1, &stmt, NULL);
	assert(rc == SQLITE_OK);

	sqlite3_bind_int64(stmt, 1, _id);
	sqlite3_bind_int64(stmt, 2, song_id);
	sqlite3_bind_int(stmt, 3, pos);

	if ((rc = sqlite3_step(stmt)) == SQLITE_ROW)
		ret = TRUE;
	else
		ret = FALSE;

	sqlite3_finalize(stmt);

	return ret;
}

static int get_playlist_length(sqlite3 *dbh, sqlite_int64 _id)
{
	sqlite3_stmt *stmt;
	int rc;
	int count;

	rc = sqlite3_prepare(dbh, "SELECT COUNT(*) FROM playlist_songs WHERE playlist_id = ?", -1, &stmt, NULL);
	assert(rc == SQLITE_OK);

	sqlite3_bind_int64(stmt, 1, _id);

	if ((rc = sqlite3_step(stmt)) == SQLITE_ROW)
	{
		count = sqlite3_column_int(stmt, 0);
		assert(count >= 0);
	}

	sqlite3_finalize(stmt);

	return count;
}

static void set_playlist_songs(sqlite3 *dbh, sqlite_int64 _id,
  GList *songs, int length)
{
	sqlite3_stmt *stmt;
	int rc;

	rc = sqlite3_prepare(dbh, "DELETE FROM playlist_songs WHERE playlist_id = ?", -1, &stmt, NULL);
	assert(rc == SQLITE_OK);

	sqlite3_bind_int64(stmt, 1, _id);

	rc = sqlite3_step(stmt);
	assert(rc == SQLITE_DONE);

	sqlite3_finalize(stmt);

	rc = sqlite3_prepare(dbh,
"INSERT INTO playlist_songs "
	"(playlist_id, song_id, position)"
"VALUES"
	"(?, ?, ?)",
	-1, &stmt, NULL);
	assert(rc == SQLITE_OK);

	sqlite3_bind_int64(stmt, 1, _id);

	while (length-- > 0)
	{
		assert(songs != NULL);
		assert(songs->data != NULL);

		sqlite_int64 *song_id = songs->data;

		sqlite3_bind_int64(stmt, 2, *song_id);
		sqlite3_bind_int(stmt, 3, length);

		rc = sqlite3_step(stmt);
		assert(rc == SQLITE_DONE);

		sqlite3_reset(stmt);
		songs = songs->next;
	}

	sqlite3_finalize(stmt);

	assert(songs == NULL);
}

static void on_playlist_changed(sqlite3 *dbh, sqlite_int64 _id)
{
	sqlite3_stmt *stmt;
	int rc;

	time_t now = time(NULL);

	rc = sqlite3_prepare(dbh,
"INSERT INTO item_changelog "
	"(timestamp, event, domain, item_id) "
"VALUES "
	"(?, ?, ?, ?)",
	-1, &stmt, NULL);
	assert(rc == SQLITE_OK);

	sqlite3_bind_int64(stmt, 1, (sqlite_int64)now);
	sqlite3_bind_int(stmt, 2, 2);
	sqlite3_bind_text(stmt, 3, "playlist", -1, SQLITE_STATIC);
	sqlite3_bind_int64(stmt, 4, _id);

	rc = sqlite3_step(stmt);
	assert(rc == SQLITE_DONE);

	sqlite3_finalize(stmt);
}

static void cleanup_and_set_playlist_songs(sqlite3 *dbh, sqlite_int64 _id,
  GList *songs, int nsongs, gboolean changed)
{
	if (_id >= 0)
	{
		if (nsongs == 0)
			mark_pending_delete(dbh, _id, TRUE);
		else
		{
			int nsongs_orig = get_playlist_length(dbh, _id);

			/* Check that the number of songs is the same (we already confirmed
			 * the positions to be the same). */
			if (changed == FALSE && nsongs_orig != nsongs)
				changed = TRUE;

			set_playlist_songs(dbh, _id, songs, nsongs);

			if (changed == TRUE)
				on_playlist_changed(dbh, _id);
		}
	}

	while (songs != NULL)
	{
		GList *next = songs->next;
		g_free(songs->data);
		g_list_free_1(songs);
		songs = next;
	}
}

sqlite_int64 playlist_renamed(sqlite3 *dbh, sqlite_int64 _id, const char *dst)
{
	sqlite3_stmt *stmt;
	int rc;

	rc = sqlite3_prepare(dbh, "UPDATE playlists SET filename = ?, name = ? WHERE _id = ?",
	  -1, &stmt, NULL);
	assert(rc == SQLITE_OK);

	sqlite3_bind_text(stmt, 1, dst, -1, SQLITE_STATIC);
	sqlite3_bind_text(stmt, 2, derive_playlist_name(dst), -1, g_free);
	sqlite3_bind_int64(stmt, 3, _id);

	rc = sqlite3_step(stmt);
	assert(rc == SQLITE_DONE);

	sqlite3_finalize(stmt);

	on_playlist_changed(dbh, _id);
}

static sqlite_int64 process_song(sqlite3 *dbh, sqlite_int64 _id,
  const char *playlistpath,
  GList **songsref, int *nsongsref, gboolean *changedref,
  const char *path, struct stat *stref)
{
	sqlite_int64 song_id;
	struct stat stbuf;
	char *pathdup = NULL;

	/* For some reason some players like to encode the path as a file:///
	 * URI. */
#if GLIB_CHECK_VERSION(2,16,0)
	if (g_ascii_strncasecmp(path, "file:///", 8) == 0)
	{
		pathdup = g_uri_unescape_string(path + 7, NULL);
		path = pathdup;
	}
	/* Handle relative paths by prepending the directory path of the
	 * playlist file. */
	else
#endif /* glib >= 2.16.0 */
	if (g_path_is_absolute(path) == FALSE)
	{
		char *playlistdir = g_path_get_dirname(playlistpath);
		pathdup = g_build_filename(playlistdir, path, NULL);
		g_free(playlistdir);
		path = pathdup;
	}

	if (stref == NULL)
	{
		if (stat(path, &stbuf) == -1)
		{
			g_warning("Couldn't stat referenced song '%s': %s", path,
			  g_strerror(errno));
			return -1;
		}

		stref = &stbuf;
	}

	if (is_music(path) == FALSE ||
	    (song_id = handle_file_song(dbh, path, stref)) == -1)
		g_warning("Couldn't parse referenced song '%s'", path);
	else
	{
		/* Try to detect if an existing playlist has any songs deleted
		 * or changed position. */
		if (_id > 0 && *changedref == FALSE &&
			has_playlist_song_at_pos(dbh, _id, song_id, *nsongsref) == FALSE)
			*changedref = TRUE;

		*songsref = g_list_prepend(*songsref,
		  g_memdup(&song_id, sizeof(song_id)));

		(*nsongsref)++;
	}

	if (pathdup != NULL)
		g_free(pathdup);

	return song_id;
}

static sqlite_int64 handle_file_m3u(sqlite3 *dbh, const char *path)
{
	FILE *fp;
	char line[PATH_MAX];
	sqlite_int64 _id;
	GList *songs = NULL;
	gboolean changed = FALSE;
	int num_songs = 0;

	if ((fp = fopen(path, "r")) == NULL)
	{
		g_warning("Couldn't open '%s': %s", path, g_strerror(errno));
		return -1;
	}

	_id = find_playlist(dbh, path);

	while (1)
	{
		sqlite_int64 song_id;

		if (fgets(line, sizeof(line), fp) == NULL)
			break;

		/* We ignore EXTM3U data. */
		if (strncmp(line, "#EXT", 4) == 0)
			continue;

		g_strchomp(line);
		process_song(dbh, _id, path, &songs, &num_songs, &changed, line, NULL);
	}

	_id = insert_or_tick_playlist(dbh, _id, path);

cleanup:
	fclose(fp);
	cleanup_and_set_playlist_songs(dbh, _id, songs, num_songs, changed);

	return _id;
}

static sqlite_int64 handle_file_pls(sqlite3 *dbh, const char *path)
{
	GKeyFile *keyfile;
	sqlite_int64 _id = -1;
	char filen[9];
	GList *songs = NULL;
	gboolean changed = FALSE;
	int num_songs = 0;
	int i;

	keyfile = g_key_file_new();
	assert(keyfile != NULL);

	if (g_key_file_load_from_file(keyfile, path, G_KEY_FILE_NONE, NULL) == FALSE)
	{
		g_warning("Playlist '%s' doesn't appear to be a .pls file", path);
		goto cleanup;
	}

	if (g_key_file_has_group(keyfile, "playlist") == FALSE)
	{
		g_warning("Playlist '%s' doesn't appear to be a .pls file: Missing [playlist] section", path);
		goto cleanup;
	}

	_id = find_playlist(dbh, path);

	strcpy(filen, "File");

	for (i = 1; i < 10000; i++)
	{
		snprintf(filen + 4, sizeof(filen) - 4, "%d", i);

		char *file = g_key_file_get_string(keyfile, "playlist", filen, NULL);

		if (file == NULL)
			break;

		process_song(dbh, _id, path, &songs, &num_songs, &changed, file, NULL);
	}

	_id = insert_or_tick_playlist(dbh, _id, path);

cleanup:
	g_key_file_free(keyfile);
	cleanup_and_set_playlist_songs(dbh, _id, songs, num_songs, changed);

	return _id;
}

sqlite_int64 handle_file_playlist(sqlite3 *dbh, const char *path)
{
	const char *ext = strrchr(path, '.');

	if (ext == NULL)
		return -1;

	ext++;

	if (g_ascii_strcasecmp(ext, "m3u") == 0)
		return handle_file_m3u(dbh, path);
	else if (g_ascii_strcasecmp(ext, "pls") == 0)
		return handle_file_pls(dbh, path);
	else
	{
		g_warning("No idea how to handle '%s'", path);
		return -1;
	}
}

/*****************************************************************************/

gboolean is_playlist(const char *path)
{
	size_t len = strlen(path);

	if (len <= 4)
		return FALSE;

	if (g_ascii_strcasecmp(path + len - 4, ".m3u") == 0)
		return TRUE;

	if (g_ascii_strcasecmp(path + len - 4, ".pls") == 0)
		return TRUE;

	return FALSE;
}
